Zodによるバリデーションに入門する
こんにちは。CX事業本部のKyoです。近いうちにZodを使うことになりそうなので素振りしておきました。
はじめに
ZodはTypeScriptの型システムを活用し、データの形状や構造を静的にチェックすることが可能なライブラリです。これにより、APIからのレスポンスやユーザーからの入力など、アプリケーションで扱うデータが期待する形状であることを保証できます。
Zod公式ドキュメントでは以下のように説明されています。
Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple stri"g to a complex nested object.
今回は公式ドキュメントからリンクされていたチュートリアルをやってみました。
全体の流れ
以下のリポジトリに問題が格納されています。私はリポジトリをCloneしてVSCodeで解きましたが、gitpodによるオンライン版も利用可能なようです。
01-number.problem.ts という形で問題のコードが与えられます。問題にはテストも含まれていますが、最初はテストが通らない状態です。これをドキュメントを読みつつ修正し、テストが通る状態にします。コードの中にヒントが書かれていたり、Webにも解説があったりと解きやすいようになっています。また、01-number.solution.tsという形で解答も付いているので安心です。
やってみた
どんな問題(ユースケース)があったのかをざっくりふりかえります。
1. Runtime Type Checking with Zod
toString
が定義されており、この引数がNumber型かどうかのバリデーションを行います。Zodの基本的な使い方ですね。
const numberParser = z.number(); export const toString = (num: unknown) => { const parsed = numberParser.parse(num); return String(parsed); };
参考: Numbers
2. Verify Unknown APIs with an Object Schema
Zodの代表的なユースケースであるAPIレスポンスのバリデーションを行います。具体的には、人物情報のAPIレスポンスがname
という文字列のプロパティを必ず含んでいることを保証します。
const PersonResult = z.object({ name: z.string(), });
参考: Object
3. Create an Array of Custom Types
APIのレスポンスが配列で人物の情報を返すことのバリデーションを行います。以下の形でAPIレスポンスが想定された型の配列であることを担保しました。
const StarWarsPeopleResults = z.object({ results: z.array(StarWarsPerson), });
参考: Arrays
4. Extract a Type from a Parser Object
APIのレスポンスが想定のTypeScriptの型であるかどうかをバリデーションします。Zodのinfer
メソッドでStarWarsPeopleResults
スキーマ(パーサー)からTypeScriptの型を生成します。これでStarWarsPeopleResultsType
というTypeScriptの型がZodのスキーマと一致することになります。
const StarWarsPeopleResults = z.object({ results: z.array(StarWarsPerson), }); type StarWarsPeopleResultsType = z.infer<typeof StarWarsPeopleResults>;
参考: Type inference
5. Make Schemas Optional
フォーム入力をお題に、name
が必須、phoneNumber
が任意というケースのバリデーションを行います。
const Form = z.object({ name: z.string(), phoneNumber: z.string().optional(), });
参考: optional
6. Set a Default Value with Zod
フォーム入力をお題に、値が存在しない場合はデフォルト値として空の配列をセットするというケースのバリデーションします。
const Form = z.object({ repoName: z.string(), keywords: z.array(z.string()).default([]), });
参考: default
7. Be Specific with Allowed Types
フォーム入力をお題に、あるプロパティが指定された値の文字列を満たすかどうかをバリデーションします。具体的にはprivacyLevel
には、private
, public
のどちらかが入っているかを(拡張性の高い方法で)検証しました。enumを使うパターンで解きましたが、unionでも良いようです。
const Form = z.object({ repoName: z.string(), privacyLevel: z.enum(["private", "public"]), });
8. Complex Schema Validation
フォーム入力をお題に、各プロパティが条件を満たしているかをバリデーションします。
満たすべき条件は以下です。
name
,phoneNumber
が規定の文字数email
,website
が不正な形でないこと
const Form = z.object({ name: z.string().min(1), phoneNumber: z.string().min(5).max(20).optional(), email: z.string().email(), website: z.string().url().optional(), });
参考: Strings
9. Reduce Duplicated Code by Composing Schemas
これまでとはやや毛色の異なる問題です。重複しているスキーマをDRYな形にリファクタリングします。元のスキーマに共通していたUUIDをObjectWithId
として切り出し、extend
でプロパティを追加しました。なお異なる型を組み合わせたい場合にはmerge
がよいようです。
const ObjectWithId = z.object({ id: z.string().uuid(), }); const User = ObjectWithId.extend({ name: z.string(), }); const Post = ObjectWithId.extend({ title: z.string(), body: z.string(), }); const Comment = ObjectWithId.extend({ text: z.string(), });
10 . Transform Data from Within a Schema
APIレスポンスのバリデーションと変換をします。具体的にはレスポンスをname
というプロパティをもったオブジェクトのスキーマとしてバリデーションして、そこからtransform
で変換を行います。この変換ロジックは、transform
の引数の無名関数が担当し、ここではname
を空文字で分割して配列に変換しています。
const StarWarsPerson = z .object({ name: z.string(), }) .transform((person) => ({ ...person, nameAsArray: person.name.split(" "), }));
参考: transform
おわりに
今回はチュートリアルを通してZodの機能と使用方法を学びました。このチュートリアルは構成が良く、自然と公式ドキュメントを読み進めて学習する流れが作られていました。またボリュームとしてもちょうどよく、理解しやすかったと思います。